package com.study.crypto.utils;

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
import org.bouncycastle.crypto.macs.GMac;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.paddings.BlockCipherPadding;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;

/**
 * sm4 加解密工具类
 * @author Songjin
 * @since 2021-02-28 10:09
 */
public class SM4Utils {
    
    public static final String ALGORITHM_NAME               = "SM4";
    public static final String ALGORITHM_NAME_ECB_PADDING   = "SM4/ECB/PKCS5Padding";
    public static final String ALGORITHM_NAME_ECB_NOPADDING = "SM4/ECB/NoPadding";
    public static final String ALGORITHM_NAME_CBC_PADDING   = "SM4/CBC/PKCS5Padding";
    public static final String ALGORITHM_NAME_CBC_NOPADDING = "SM4/CBC/NoPadding";
    
    private SM4Utils() {
    }
    
    /**
     * SM4算法目前只支持128位（即密钥16字节）
     */
    public static final int DEFAULT_KEY_SIZE = 128;
    
    static {
        Security.insertProviderAt(new BouncyCastleProvider(), 1);
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 产生 sm4 对称密钥
     * @return 对称密钥
     * @throws NoSuchAlgorithmException 异常
     * @throws NoSuchProviderException  异常
     */
    public static byte[] generateKey() throws NoSuchAlgorithmException, NoSuchProviderException {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    /**
     * 产生对称密钥
     * @param keySize 密钥位数
     * @return 对称密钥
     * @throws NoSuchAlgorithmException 异常
     * @throws NoSuchProviderException  异常
     */
    public static byte[] generateKey(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    /**
     * 使用 ECB 带填充方式加密
     * @param key  对称密钥
     * @param data 原文数据
     * @return 密文
     * @throws InvalidKeyException       异常
     * @throws NoSuchAlgorithmException  异常
     * @throws NoSuchProviderException   异常
     * @throws NoSuchPaddingException    异常
     * @throws IllegalBlockSizeException 异常
     * @throws BadPaddingException       异常
     */
    public static byte[] encrypt_ecb_padding(byte[] key, byte[] data)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    /**
     * 使用 ECB 带填充方式解密
     * @param key        对称密钥
     * @param ciphertext 密文数据
     * @return 原文
     * @throws IllegalBlockSizeException 异常
     * @throws BadPaddingException       异常
     * @throws InvalidKeyException       异常
     * @throws NoSuchAlgorithmException  异常
     * @throws NoSuchProviderException   异常
     * @throws NoSuchPaddingException    异常
     */
    public static byte[] decrypt_ecb_padding(byte[] key, byte[] ciphertext)
            throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException,
            NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
        Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(ciphertext);
    }

    /**
     * 使用 ECB 无填充方式加密
     * @param key  对称密钥
     * @param data 原文数据
     * @return 密文
     * @throws InvalidKeyException       异常
     * @throws NoSuchAlgorithmException  异常
     * @throws NoSuchProviderException   异常
     * @throws NoSuchPaddingException    异常
     * @throws IllegalBlockSizeException 异常
     * @throws BadPaddingException       异常
     */
    public static byte[] encrypt_ecb_nopadding(byte[] key, byte[] data)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    /**
     * 使用 ECB 无填充方式解密
     * @param key        对称密钥
     * @param ciphertext 密文数据
     * @return 原文
     * @throws IllegalBlockSizeException 异常
     * @throws BadPaddingException       异常
     * @throws InvalidKeyException       异常
     * @throws NoSuchAlgorithmException  异常
     * @throws NoSuchProviderException   异常
     * @throws NoSuchPaddingException    异常
     */
    public static byte[] decrypt_ecb_nopadding(byte[] key, byte[] ciphertext)
            throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException,
            NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
        Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(ciphertext);
    }

    /**
     * 使用 ECB 带填充方式加密
     *
     * @param key  对称密钥
     * @param iv   初始化向量
     * @param data 原文
     * @return 密文
     * @throws InvalidKeyException                异常
     * @throws NoSuchAlgorithmException           异常
     * @throws NoSuchProviderException            异常
     * @throws NoSuchPaddingException             异常
     * @throws IllegalBlockSizeException          异常
     * @throws BadPaddingException                异常
     * @throws InvalidAlgorithmParameterException 异常
     */
    public static byte[] encrypt_cbc_padding(byte[] key, byte[] iv, byte[] data)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException,
            InvalidAlgorithmParameterException {
        Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }

    /**
     * 使用 CBC 带填充方式解密
     * @param key        对称密钥
     * @param iv         初始化向量
     * @param ciphertext 密文
     * @return 原文
     * @throws InvalidKeyException                异常
     * @throws NoSuchAlgorithmException           异常
     * @throws NoSuchProviderException            异常
     * @throws NoSuchPaddingException             异常
     * @throws IllegalBlockSizeException          异常
     * @throws BadPaddingException                异常
     * @throws InvalidAlgorithmParameterException 异常
     */
    public static byte[] decrypt_cbc_padding(byte[] key, byte[] iv, byte[] ciphertext)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
        Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, key, iv);
        return cipher.doFinal(ciphertext);
    }

    /**
     * 使用 CBC 无填充方式加密
     * @param key        对称密钥
     * @param iv         初始化向量
     * @param data 原文
     * @return 密文
     * @throws InvalidKeyException                异常
     * @throws NoSuchAlgorithmException           异常
     * @throws NoSuchProviderException            异常
     * @throws NoSuchPaddingException             异常
     * @throws IllegalBlockSizeException          异常
     * @throws BadPaddingException                异常
     * @throws InvalidAlgorithmParameterException 异常
     */
    public static byte[] encrypt_cbc_nopadding(byte[] key, byte[] iv, byte[] data)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
        Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.ENCRYPT_MODE, key, iv);
        return cipher.doFinal(data);
    }

    /**
     * 使用 CBC 无填充方式解密
     * @param key        对称密钥
     * @param iv         初始化向量
     * @param ciphertext 密文
     * @return 原文
     * @throws InvalidKeyException                异常
     * @throws NoSuchAlgorithmException           异常
     * @throws NoSuchProviderException            异常
     * @throws NoSuchPaddingException             异常
     * @throws IllegalBlockSizeException          异常
     * @throws BadPaddingException                异常
     * @throws InvalidAlgorithmParameterException 异常
     */
    public static byte[] decrypt_cbc_nopadding(byte[] key, byte[] iv, byte[] ciphertext)
            throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException,
            NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
            InvalidAlgorithmParameterException {
        Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.DECRYPT_MODE, key, iv);
        return cipher.doFinal(ciphertext);
    }
    
    public static byte[] doCMac(byte[] key, byte[] data) throws NoSuchProviderException, NoSuchAlgorithmException,
            InvalidKeyException {
        Key keyObj = new SecretKeySpec(key, ALGORITHM_NAME);
        return doMac("SM4-CMAC", keyObj, data);
    }
    
    public static byte[] doGMac(byte[] key, byte[] iv, int tagLength, byte[] data) {
        org.bouncycastle.crypto.Mac mac = new GMac(new GCMBlockCipher(new SM4Engine()), tagLength * 8);
        return doMac(mac, key, iv, data);
    }

    /**
     * 默认使用PKCS7Padding/PKCS5Padding填充的CBCMAC
     * @param key  密钥
     * @param iv   初始化向量
     * @param data 数据
     * @return CBCMAC值
     */
    public static byte[] doCBCMac(byte[] key, byte[] iv, byte[] data) {
        SM4Engine engine = new SM4Engine();
        org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, new PKCS7Padding());
        return doMac(mac, key, iv, data);
    }
    
    /**
     * @param key 密钥
     * @param iv 初始化向量
     * @param padding 可以传null，传null表示NoPadding，由调用方保证数据必须是BlockSize的整数倍
     * @param data 数据
     * @return CBCMAC值
     * @throws Exception 异常
     */
    public static byte[] doCBCMac(byte[] key, byte[] iv, BlockCipherPadding padding, byte[] data) throws Exception {
        SM4Engine engine = new SM4Engine();
        if (padding == null) {
            if (data.length % engine.getBlockSize() != 0) {
                throw new Exception("if no padding, data length must be multiple of SM4 BlockSize");
            }
        }
        org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, padding);
        return doMac(mac, key, iv, data);
    }
    
    
    private static byte[] doMac(org.bouncycastle.crypto.Mac mac, byte[] key, byte[] iv, byte[] data) {
        CipherParameters cipherParameters = new KeyParameter(key);
        mac.init(new ParametersWithIV(cipherParameters, iv));
        mac.update(data, 0, data.length);
        byte[] result = new byte[mac.getMacSize()];
        mac.doFinal(result, 0);
        return result;
    }
    
    private static byte[] doMac(String algorithmName, Key key, byte[] data) throws NoSuchProviderException,
            NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        mac.init(key);
        mac.update(data);
        return mac.doFinal();
    }
    
    private static Cipher generateECBCipher(String algorithmName, int mode, byte[] key)
            throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException {
        Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key    sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }
    
    private static Cipher generateCBCCipher(String algorithmName, int mode, byte[] key, byte[] iv)
            throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            NoSuchProviderException, NoSuchPaddingException {
        Cipher          cipher          = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key             sm4Key          = new SecretKeySpec(key, ALGORITHM_NAME);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        cipher.init(mode, sm4Key, ivParameterSpec);
        return cipher;
    }
    
    /**
     * SM4 对称加密补位，补位成 16 字节整数倍
     * @param data 原文
     * @return 补位后的字节数据
     */
    public static byte[] complement(byte[] data) {
        int len = data.length;
        byte key = (byte) (16 - len % 16);
        byte[] rtn = new byte[len + key];
        System.arraycopy(data, 0, rtn, 0, data.length);
        for (int i = 0; i < key; ++i) {
            rtn[data.length + i] = key;
        }
        return rtn;
    }
}
